﻿using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Text.RegularExpressions;
using System.Xml;

namespace CC_NSISrunner
{
    internal class NsisXmlFormatter
    {
        private const int OFFSET_STEP = 4;

        private string version;
        private string pluginsPath;
        private string workDirectory;
        private string workScript;
        private string processedFiles;
        private string outputFile;

        readonly Regex regexNsisVersion = new Regex(@"MakeNSIS\s+v(\d+\.\d+)");
        readonly Regex regexNsisPluginsBegin = new Regex("Processing plugin dlls:\\s+\"(.+?)\"");
        readonly Regex regexNsisChangingDirectory = new Regex("^Changing directory to:\\s+\"(.+?)\"");
        readonly Regex regexNsisScriptBegin = new Regex("^Processing script file:\\s+\"(.+?)\"");
        readonly Regex regexNsisProcessedBegin = new Regex(@"^Processed\s+(\d+)");
        readonly Regex regexNsisOutputBegin = new Regex("^Output:\\s+\"(.+?)\"");

        readonly Regex regexNsisScriptEnd = new Regex("^SectionEnd");
        readonly Regex regexNsisProcessedEnd = new Regex("^Output:");
        readonly Regex regexNsisPluginLine = new Regex(@"^\s+-\s+(\S+)");

        List<string> plugins = new List<string>();
        List<string> scriptInfos = new List<string>();
        List<string> processedInfos = new List<string>();
        List<string> outputInfos = new List<string>();
        List<string> errors = new List<string>();

        private readonly NsisOutputStateMachine stateMachine = new NsisOutputStateMachine();
        private string xmlFileName;

        public NsisXmlFormatter(string xmlFileName)
        {
            this.xmlFileName = xmlFileName;
        }

        public NsisXmlFormatter()
        {
            xmlFileName = "test.xml";
        }

        #region Parsing
        public void ParseInput(string line)
        {
            if (line == null)
            {
                stateMachine.SetState(NsisOutputStateMachine.EState.OutputEnd);
            }
            else
            {
                stateMachine.SetNewLine();
                if (!ProcessEndPoints(line))
                {
                    ProcessStartPoints(line);
                }
                if (ExtractValue(line, regexNsisOutputBegin, ref outputFile))
                {
                    stateMachine.SetState(NsisOutputStateMachine.EState.OutputBegin);
                }
            }
            DoLastState(line);
        }

        private void AddLine(List<string> infos, string line)
        {
            if (string.IsNullOrEmpty(line))
            {
                return;
            }
            line = line.Trim();
            if (line.Length == 0 || line[0] == '\n' || line[0] == '\r')
            {
                return;
            }
            infos.Add(line);
        }

        private void DoLastState(string line)
        {
            switch (stateMachine.CurrentState)
            {
                case NsisOutputStateMachine.EState.None:
                    break;
                case NsisOutputStateMachine.EState.VersionFound:
                    break;
                case NsisOutputStateMachine.EState.PluginsBegin:
                    string pluginName = string.Empty;
                    if (IsPluginLine(line,ref pluginName))
                    {
                        AddLine(plugins, pluginName);
                    }
                    else if (stateMachine.IsNewLineAfterState)
                    {
                        stateMachine.SetState(NsisOutputStateMachine.EState.PluginsEnd);
                    }
                    break;
                case NsisOutputStateMachine.EState.PluginsEnd:
                    line = line.Trim();
                    if (stateMachine.IsNewLineAfterState && line.Length >0 && line.IndexOf("!define", StringComparison.Ordinal) < 0)
                    {
                        AddLine(errors, line);
                    }
                    break;
                case NsisOutputStateMachine.EState.ChangingDirectory:
                    break;
                case NsisOutputStateMachine.EState.ScriptBegin:
                    if (stateMachine.IsNewLineAfterState)
                    {
                        AddLine(scriptInfos, line);
                    }
                    break;
                case NsisOutputStateMachine.EState.ScriptEnd:
                    break;
                case NsisOutputStateMachine.EState.ProcessedBegin:
                    if (stateMachine.IsNewLineAfterState)
                    {
                        AddLine(processedInfos, line);
                    }
                    break;
                case NsisOutputStateMachine.EState.ProcessedEnd:
                    break;
                case NsisOutputStateMachine.EState.OutputBegin:
                    if (stateMachine.IsNewLineAfterState)
                    {
                        AddLine(outputInfos, line);
                    }
                    break;
                case NsisOutputStateMachine.EState.OutputEnd:
                    ProduceXml();
                    break;
                case NsisOutputStateMachine.EState.Error:
                    break;
                default:
                    throw new ArgumentOutOfRangeException();
            }
            if (line != null && line.IndexOf("error", System.StringComparison.Ordinal) > 0)
            {
                AddLine(errors, line);
            }
        }

        public void SetErrorState()
        {
            stateMachine.SetState(NsisOutputStateMachine.EState.Error);
        }

        private void ProcessStartPoints(string line)
        {
            if (ExtractValue(line, regexNsisVersion, ref version))
            {
                stateMachine.SetState(NsisOutputStateMachine.EState.VersionFound);
            }
            else
            {
                if (ExtractValue(line, regexNsisPluginsBegin, ref pluginsPath))
                {
                    stateMachine.SetState(NsisOutputStateMachine.EState.PluginsBegin);
                }
                else
                {
                    if (ExtractValue(line, regexNsisChangingDirectory, ref workDirectory))
                    {
                        stateMachine.SetState(NsisOutputStateMachine.EState.ChangingDirectory);
                    }
                    else
                    {
                        if (ExtractValue(line, regexNsisScriptBegin, ref workScript))
                        {
                            stateMachine.SetState(NsisOutputStateMachine.EState.ScriptBegin);
                        }
                        else
                        {
                            if (ExtractValue(line, regexNsisProcessedBegin, ref processedFiles))
                            {
                                stateMachine.SetState(NsisOutputStateMachine.EState.ProcessedBegin);
                            }
                        }
                    }
                }
            }
        }

        private bool ProcessEndPoints(string line)
        {
            bool ret = false;
            if (IsScriptEnd(line))
            {
                stateMachine.SetState(NsisOutputStateMachine.EState.ScriptEnd);
                ret = true;
            }
            else if (IsProcessedEnd(line))
            {
                stateMachine.SetState(NsisOutputStateMachine.EState.ProcessedEnd);
                ret = true;
            }
            return ret;
        }

        private bool IsScriptEnd(string line)
        {
            string temp = "";
            return ExtractValue(line, regexNsisScriptEnd, ref temp);
        }

        private bool IsProcessedEnd(string line)
        {
            string temp = "";
            return ExtractValue(line, regexNsisProcessedEnd, ref temp);
        }

        private bool IsPluginLine(string line, ref string pluginName)
        {
            return ExtractValue(line, regexNsisPluginLine, ref pluginName);
        }

        private bool ExtractValue(string line, Regex regex, ref string extractedPart)
        {
            bool ret = false;
            MatchCollection matchCollection = regex.Matches(line);
            if (matchCollection.Count > 0)
            {
                Match match = matchCollection[0];
                if (match.Groups.Count > 0)
                {
                    Group matchGroup1 = match.Groups[1];
                    extractedPart = matchGroup1.ToString();
                }
                ret = true;
            }
            return ret;
        }
        #endregion Parsing

        #region XML

        public static string Indent(int count)
        {
            return "".PadLeft(count);
        }

        private void ProduceXml()
        {
            XmlDocument doc = new XmlDocument();
            XmlNode root;
            XmlNode node;
            XmlAttribute attribute;

            root = doc.CreateElement("NSIS");
            doc.AppendChild(root);

            attribute = doc.CreateAttribute("version"); 
            attribute.Value = Version;
            root.Attributes.Append(attribute);

            node = doc.CreateElement("Configuration");
            attribute = doc.CreateAttribute("plugins_dir"); 
            attribute.Value = PluginsPath;
            node.Attributes.Append(attribute);

            root.AppendChild(node);

            AddChildren(doc, node, "plugin", plugins);

            node = doc.CreateElement("Processed");
            attribute = doc.CreateAttribute("number"); 
            attribute.Value = ProcessedFiles;
            node.Attributes.Append(attribute);
            attribute = doc.CreateAttribute("script_file"); 
            attribute.Value = WorkScript;
            node.Attributes.Append(attribute);
            root.AppendChild(node);
            AddChildren(doc, node, "script", scriptInfos);

            node = doc.CreateElement("Output");
            attribute = doc.CreateAttribute("installer_file"); 
            attribute.Value = OutputFile;
            node.Attributes.Append(attribute);
            root.AppendChild(node);
            AddChildren(doc, node, "buildStep", processedInfos);

            node = doc.CreateElement("Installer");
            root.AppendChild(node);
            AddChildren(doc, node, "installerInfo", outputInfos);

            node = doc.CreateElement("Errors");
            attribute = doc.CreateAttribute("errors_count");
            attribute.Value = errors.Count.ToString(CultureInfo.InvariantCulture);
            node.Attributes.Append(attribute);
            root.AppendChild(node);
            AddChildren(doc, node, "Error", errors);

            doc.Save(xmlFileName);

            //// create a writer and open the file
            //TextWriter textWriter = new StreamWriter(xmlFileName);

            //// write a line of text to the file
            //textWriter.WriteLine(DateTime.Now);

            //// close the stream
            //textWriter.Close();
        }

        private void AddChildren(XmlDocument doc, XmlNode node, string childTagName, List<string> lines)
        {
            foreach (string plugin in lines)
            {
                XmlNode child = doc.CreateElement(childTagName);
                XmlText xmlText = doc.CreateTextNode(plugin);
                child.AppendChild(xmlText);
                node.AppendChild(child);
            }
        }

        void WriteTag(TextWriter textWriter, int offset, string tagName, string value)
        {
            textWriter.Write(string.Format("{0}<{1}>{2}</{1}>",Indent(offset), tagName,value));
        }

        void WriteTag(TextWriter textWriter, int offset, string tagParent, string tagName, List<string> values)
        {
            textWriter.Write(string.Format("{0}<{1}>", Indent(offset), tagParent));
            offset += OFFSET_STEP;
            foreach (string value in values)
            {
                WriteTag(textWriter, offset, tagName, value);
            }
            offset -= OFFSET_STEP;

            textWriter.Write(string.Format("{0}</{1}>", Indent(offset), tagName));
        }

        #endregion XML

        public string XmlFileName
        {
            get { return xmlFileName; }
        }

        public string Version
        {
            get { return version; }
        }

        public string PluginsPath
        {
            get { return pluginsPath; }
        }

        public string WorkDirectory
        {
            get { return workDirectory; }
        }

        public string WorkScript
        {
            get { return workScript; }
        }

        public string ProcessedFiles
        {
            get { return processedFiles; }
        }

        public string OutputFile
        {
            get { return outputFile; }
        }

    }
}
